package io.github.vinceglb.filekit

import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.documentfile.provider.DocumentFile
import io.github.vinceglb.filekit.exceptions.FileKitException
import io.github.vinceglb.filekit.exceptions.FileKitUriPathNotSupportedException
import io.github.vinceglb.filekit.mimeType.MimeType
import io.github.vinceglb.filekit.utils.div
import io.github.vinceglb.filekit.utils.toKotlinxPath
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.io.RawSink
import kotlinx.io.RawSource
import kotlinx.io.asSink
import kotlinx.io.asSource
import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import kotlinx.serialization.Serializable
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.BasicFileAttributes
import kotlin.time.ExperimentalTime
import kotlin.time.Instant

/**
 * Represents a file on the Android platform.
 *
 * This class wraps either a [File] (for filesystem paths) or a [Uri] (for content providers).
 *
 * @property androidFile The underlying [AndroidFile] wrapper (File or Uri).
 */
@Serializable(with = PlatformFileSerializer::class)
public actual data class PlatformFile(
    val androidFile: AndroidFile,
) {
    public actual override fun toString(): String = path

    public actual companion object
}

/**
 * Wrapper for Android file representations.
 */
public sealed class AndroidFile {
    /**
     * Wraps a standard Java [File].
     */
    public data class FileWrapper(
        val file: File,
    ) : AndroidFile()

    /**
     * Wraps an Android [Uri].
     */
    public data class UriWrapper(
        val uri: Uri,
    ) : AndroidFile()
}

public actual fun PlatformFile(path: Path): PlatformFile =
    PlatformFile(AndroidFile.FileWrapper(File(path.toString())))

/**
 * Creates a [PlatformFile] from an Android [Uri].
 *
 * @param uri The [Uri] to wrap.
 * @return A [PlatformFile] instance.
 */
public fun PlatformFile(uri: Uri): PlatformFile =
    PlatformFile(AndroidFile.UriWrapper(uri))

/**
 * Creates a [PlatformFile] from a Java [File].
 *
 * @param file The [File] to wrap.
 * @return A [PlatformFile] instance.
 */
public fun PlatformFile(file: File): PlatformFile =
    PlatformFile(AndroidFile.FileWrapper(file))

public actual fun PlatformFile(path: String): PlatformFile {
    // If the path looks like an Android Uri ("content://" or "file://" scheme),
    // parse it accordingly, otherwise treat it as a regular filesystem path.
    return if (path.startsWith("content://", ignoreCase = true) ||
        path.startsWith("file://", ignoreCase = true)
    ) {
        @SuppressLint("UseKtx")
        PlatformFile(AndroidFile.UriWrapper(Uri.parse(path)))
    } else {
        PlatformFile(AndroidFile.FileWrapper(File(path)))
    }
}

public actual fun PlatformFile.toKotlinxIoPath(): Path = when (androidFile) {
    is AndroidFile.FileWrapper -> androidFile.file.toKotlinxPath()
    is AndroidFile.UriWrapper -> throw FileKitUriPathNotSupportedException()
}

public actual val PlatformFile.name: String
    get() = when (androidFile) {
        is AndroidFile.FileWrapper -> toKotlinxIoPath().name
        is AndroidFile.UriWrapper -> getUriFileName(androidFile.uri)
    }

public actual val PlatformFile.extension: String
    get() = when (androidFile) {
        is AndroidFile.FileWrapper -> androidFile.file.extension

        is AndroidFile.UriWrapper -> when {
            isDirectory() -> ""
            else -> getUriFileName(androidFile.uri).substringAfterLast(".", "")
        }
    }

public actual val PlatformFile.nameWithoutExtension: String
    get() = when (androidFile) {
        is AndroidFile.FileWrapper -> androidFile.file.nameWithoutExtension

        is AndroidFile.UriWrapper -> when {
            isDirectory() -> getUriFileName(androidFile.uri)
            else -> getUriFileName(androidFile.uri).substringBeforeLast(".", "")
        }
    }

public actual val PlatformFile.path: String
    get() = when (androidFile) {
        is AndroidFile.FileWrapper -> toKotlinxIoPath().toString()
        is AndroidFile.UriWrapper -> androidFile.uri.toString()
    }

public actual fun PlatformFile.isRegularFile(): Boolean = when (androidFile) {
    is AndroidFile.FileWrapper -> {
        SystemFileSystem.metadataOrNull(toKotlinxIoPath())?.isRegularFile
            ?: false
    }

    is AndroidFile.UriWrapper -> {
        DocumentFile
            .fromSingleUri(
                FileKit.context,
                androidFile.uri,
            )?.isFile == true
    }
}

public actual fun PlatformFile.isDirectory(): Boolean = when (androidFile) {
    is AndroidFile.FileWrapper -> SystemFileSystem.metadataOrNull(toKotlinxIoPath())?.isDirectory
        ?: false

    is AndroidFile.UriWrapper -> try {
        DocumentFile
            .fromTreeUri(
                FileKit.context,
                androidFile.uri,
            )?.isDirectory == true
    } catch (_: Exception) {
        false
    }
}

public actual fun PlatformFile.isAbsolute(): Boolean = when (androidFile) {
    is AndroidFile.FileWrapper -> toKotlinxIoPath().isAbsolute
    is AndroidFile.UriWrapper -> true
}

public actual fun PlatformFile.exists(): Boolean = when (androidFile) {
    is AndroidFile.FileWrapper -> SystemFileSystem.exists(toKotlinxIoPath())
    is AndroidFile.UriWrapper -> getDocumentFile(androidFile.uri)?.exists() == true
}

public actual fun PlatformFile.size(): Long = when (androidFile) {
    is AndroidFile.FileWrapper -> SystemFileSystem.metadataOrNull(toKotlinxIoPath())?.size ?: -1
    is AndroidFile.UriWrapper -> getUriFileSize(androidFile.uri) ?: -1
}

public actual fun PlatformFile.parent(): PlatformFile? = when (androidFile) {
    is AndroidFile.FileWrapper -> {
        toKotlinxIoPath().parent?.let(::PlatformFile)
    }

    is AndroidFile.UriWrapper -> {
        val uri = androidFile.uri
        val parentUri = uri
            .buildUpon()
            .path(uri.path?.substringBeforeLast('/'))
            .build()

        DocumentFile.fromTreeUri(FileKit.context, parentUri)?.let {
            PlatformFile(it.uri)
        }
    }
}

public actual fun PlatformFile.absolutePath(): String = when (androidFile) {
    is AndroidFile.FileWrapper -> androidFile.file.absolutePath
    is AndroidFile.UriWrapper -> androidFile.uri.toString()
}

public actual fun PlatformFile.absoluteFile(): PlatformFile = when (androidFile) {
    is AndroidFile.FileWrapper -> PlatformFile(SystemFileSystem.resolve(toKotlinxIoPath()))
    is AndroidFile.UriWrapper -> this
}

@OptIn(ExperimentalTime::class)
public actual fun PlatformFile.createdAt(): Instant? = this.androidFile.let { androidFile ->
    when (androidFile) {
        is AndroidFile.FileWrapper -> {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val attributes = Files.readAttributes(
                    androidFile.file.toPath(),
                    BasicFileAttributes::class.java,
                )
                val timestamp = attributes.creationTime().toMillis()
                Instant.fromEpochMilliseconds(timestamp)
            } else {
                // Fallback for older Android versions
                null
            }
        }

        is AndroidFile.UriWrapper -> {
            null
        }
    }
}

@OptIn(ExperimentalTime::class)
public actual fun PlatformFile.lastModified(): Instant {
    val timestamp = this.androidFile.let { androidFile ->
        when (androidFile) {
            is AndroidFile.FileWrapper -> {
                androidFile.file.lastModified()
            }

            is AndroidFile.UriWrapper -> {
                DocumentFile
                    .fromSingleUri(FileKit.context, androidFile.uri)
                    ?.lastModified()
                    ?: throw IllegalStateException("Unable to get last modified date for URI")
            }
        }
    }

    return Instant.fromEpochMilliseconds(timestamp)
}

public actual fun PlatformFile.mimeType(): MimeType? {
    if (isDirectory()) {
        return null
    }

    return when (androidFile) {
        is AndroidFile.FileWrapper -> {
            val mimeTypeValue = getMimeTypeValueFromExtension(extension)
            mimeTypeValue?.let(MimeType::parse)
        }

        is AndroidFile.UriWrapper -> {
            val mimeTypeValueFromContentResolver =
                FileKit.context.contentResolver.getType(androidFile.uri)
            val mimeTypeValue =
                mimeTypeValueFromContentResolver ?: getMimeTypeValueFromExtension(extension)
            mimeTypeValue?.let(MimeType::parse)
        }
    }
}

private fun getMimeTypeValueFromExtension(extension: String): String? {
    val safeExtension = extension.trim().lowercase()

    if (safeExtension.isEmpty()) return null

    return MimeTypeMap
        .getSingleton()
        .getMimeTypeFromExtension(safeExtension)
        ?.trim()
        ?.lowercase()
}

private const val DEFAULT_STREAM_MIME_TYPE = "application/octet-stream"

internal actual suspend fun PlatformFile.prepareDestinationForWrite(
    source: PlatformFile,
): PlatformFile = withContext(Dispatchers.IO) {
    if (!isDirectory()) {
        return@withContext this@prepareDestinationForWrite
    }

    when (val target = androidFile) {
        is AndroidFile.FileWrapper -> {
            val path = toKotlinxIoPath() / source.name
            PlatformFile(path)
        }

        is AndroidFile.UriWrapper -> {
            val context = FileKit.context

            val directoryDocument = DocumentFile.fromTreeUri(context, target.uri)
                ?: DocumentFile.fromSingleUri(context, target.uri)
                ?: throw FileKitException("Could not access Uri as DocumentFile")

            directoryDocument.findFile(source.name)?.let { existing ->
                if (existing.isDirectory) {
                    throw FileKitException("Destination already contains a directory named ${source.name}")
                }

                return@withContext PlatformFile(existing.uri)
            }

            val mimeType = resolveMimeTypeForCopy(source)
            val created = directoryDocument.createFile(mimeType, source.name)
                ?: throw FileKitException("Could not create destination file in bookmarked directory")

            PlatformFile(created.uri)
        }
    }
}

private fun resolveMimeTypeForCopy(source: PlatformFile): String = source.mimeType()?.toString()
    ?: getMimeTypeValueFromExtension(source.extension)
    ?: DEFAULT_STREAM_MIME_TYPE

public actual fun PlatformFile.source(): RawSource = when (androidFile) {
    is AndroidFile.FileWrapper -> {
        SystemFileSystem.source(toKotlinxIoPath())
    }

    is AndroidFile.UriWrapper -> {
        FileKit.context.contentResolver
            .openInputStream(androidFile.uri)
            ?.asSource()
            ?: throw FileKitException("Could not open input stream for Uri")
    }
}

public actual fun PlatformFile.sink(append: Boolean): RawSink = when (androidFile) {
    is AndroidFile.FileWrapper -> {
        SystemFileSystem.sink(toKotlinxIoPath(), append)
    }

    is AndroidFile.UriWrapper -> {
        // Use "wt" (write+truncate) for overwrite, "wa" (write+append) for append
        // This ensures existing file content is properly truncated when overwriting
        val mode = if (append) "wa" else "wt"
        FileKit.context.contentResolver
            .openOutputStream(androidFile.uri, mode)
            ?.asSink()
            ?: throw FileKitException("Could not open output stream for Uri")
    }
}

public actual fun PlatformFile.startAccessingSecurityScopedResource(): Boolean = true

public actual fun PlatformFile.stopAccessingSecurityScopedResource() {}

public actual inline fun PlatformFile.list(block: (List<PlatformFile>) -> Unit) {
    when (androidFile) {
        is AndroidFile.FileWrapper -> {
            val directoryFiles = SystemFileSystem.list(toKotlinxIoPath()).map(::PlatformFile)
            block(directoryFiles)
        }

        is AndroidFile.UriWrapper -> {
            val documentFile = DocumentFile.fromTreeUri(FileKit.context, androidFile.uri)
                ?: throw FileKitException("Could not access Uri as DocumentFile")
            val files = documentFile.listFiles().map { PlatformFile(it.uri) }
            block(files)
        }
    }
}

public actual fun PlatformFile.list(): List<PlatformFile> = when (androidFile) {
    is AndroidFile.FileWrapper -> {
        SystemFileSystem.list(toKotlinxIoPath()).map(::PlatformFile)
    }

    is AndroidFile.UriWrapper -> {
        val documentFile = DocumentFile.fromTreeUri(FileKit.context, androidFile.uri)
            ?: throw FileKitException("Could not access Uri as DocumentFile")
        documentFile.listFiles().map { PlatformFile(it.uri) }
    }
}

public actual suspend fun PlatformFile.atomicMove(destination: PlatformFile): Unit =
    withContext(Dispatchers.IO) {
        when {
            androidFile is AndroidFile.FileWrapper && destination.androidFile is AndroidFile.FileWrapper -> {
                SystemFileSystem.atomicMove(
                    source = toKotlinxIoPath(),
                    destination = destination.toKotlinxIoPath(),
                )
            }

            else -> {
                // TODO only rename the file / folder

                if (isDirectory()) {
                    throw FileKitException("atomicMove does not support moving directories with Uri for now")
                }

                copyTo(destination)
                delete()
            }
        }
    }

public actual suspend fun PlatformFile.delete(mustExist: Boolean): Unit =
    withContext(Dispatchers.IO) {
        when (androidFile) {
            is AndroidFile.FileWrapper -> {
                SystemFileSystem.delete(
                    path = toKotlinxIoPath(),
                    mustExist = mustExist,
                )
            }

            is AndroidFile.UriWrapper -> {
                val documentFile = DocumentFile.fromSingleUri(FileKit.context, androidFile.uri)
                    ?: throw FileKitException("Could not access Uri as DocumentFile")

                if (documentFile.exists()) {
                    documentFile.delete()
                } else if (mustExist) {
                    throw FileKitException("Uri does not exist")
                }
            }
        }
    }

@Deprecated("Please do not use this anymore. Keep it for backward compatibility only.")
private const val BOOKMARK_FILE_PREFIX = "<<file>>"

@Deprecated("Please do not use this anymore. Keep it for backward compatibility only.")
private const val BOOKMARK_URI_PREFIX = "<<uri>>"

public actual suspend fun PlatformFile.bookmarkData(): BookmarkData = withContext(Dispatchers.IO) {
    when (androidFile) {
        is AndroidFile.FileWrapper -> {
            val data = androidFile.file.path
            BookmarkData(data.encodeToByteArray())
        }

        is AndroidFile.UriWrapper -> {
            val uri = androidFile.uri

            val flags =
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION

            // Check if this is a tree URI (directory) or document URI (file)
            val uriToPermission = if (isDirectory()) {
                // For directories, we need to get the tree URI
                val documentId = DocumentsContract.getTreeDocumentId(uri)
                val authority = uri.authority ?: throw FileKitException("Uri authority is null")
                DocumentsContract.buildTreeDocumentUri(authority, documentId)
            } else {
                // For files, use the URI directly
                uri
            }

            FileKit.context.contentResolver.takePersistableUriPermission(uriToPermission, flags)
            val data = androidFile.uri.toString()
            BookmarkData(data.encodeToByteArray())
        }
    }
}

public actual fun PlatformFile.releaseBookmark() {
    when (androidFile) {
        is AndroidFile.FileWrapper -> {}

        // No action needed for regular files
        is AndroidFile.UriWrapper -> {
            val uriToRelease = androidFile.uri.getUriToRelease(isDirectory())
            val flags =
                Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
            FileKit.context.contentResolver.releasePersistableUriPermission(uriToRelease, flags)
        }
    }
}

private fun Uri.getUriToRelease(isDirectory: Boolean): Uri = if (isDirectory) {
    val authority = this.authority ?: throw FileKitException("Uri authority is null")
    val documentId = DocumentsContract.getTreeDocumentId(this)
    DocumentsContract.buildTreeDocumentUri(authority, documentId)
} else {
    this
}

public actual fun PlatformFile.Companion.fromBookmarkData(
    bookmarkData: BookmarkData,
): PlatformFile {
    val str = bookmarkData.bytes.decodeToString()

    val platformFile = when {
        // Content Uri always starts with "content://"
        str.startsWith("content://") -> {
            val uriString = str
            @SuppressLint("UseKtx")
            PlatformFile(Uri.parse(uriString))
        }

        // Very rarely used Uri, discouraged and deprecated, may starts with "file://"
        str.startsWith("file://") -> {
            val filePath = str.removePrefix("file://")
            PlatformFile(File(filePath))
        }

        // TODO remove this in future
        str.startsWith(BOOKMARK_URI_PREFIX) -> {
            val uriString = str.removePrefix(BOOKMARK_URI_PREFIX)
            @SuppressLint("UseKtx")
            PlatformFile(Uri.parse(uriString))
        }

        str.startsWith(BOOKMARK_FILE_PREFIX) -> {
            val filePath = str.removePrefix(BOOKMARK_FILE_PREFIX)
            PlatformFile(File(filePath))
        }

        // Removes a chance of exception throwing
        else -> {
            val filePath = str
            PlatformFile(File(filePath))
        }
    }

    if (!platformFile.exists()) {
        throw FileKitException("Bookmark target is no longer accessible")
    }

    return platformFile
}

private fun getUriFileSize(uri: Uri): Long? = FileKit.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
    val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
    if (cursor.moveToFirst()) cursor.getLong(sizeIndex) else null
}

private fun getUriFileName(uri: Uri): String {
    return FileKit.context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
        val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        if (cursor.moveToFirst()) cursor.getString(nameIndex) else null
    } ?: uri.lastPathSegment ?: "" // Fallback to the Uri's last path segment
}

private fun getDocumentFile(uri: Uri): DocumentFile? = DocumentFile.fromSingleUri(FileKit.context, uri)
    ?: DocumentFile.fromTreeUri(FileKit.context, uri)
